/*
 * Lenovo Group
 * Copyright (c) 2015-2016 All Rights Reserved.
 * 
 * Project Name:itsm-gateway
 * Create Time: 2016年1月31日 上午12:02:09
 */
package com.lenovo.lmrp.server.itsmgateway.domain.metadata;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lenovo.lmrp.api.constant.APIConstant.MappedDT;
import com.lenovo.lmrp.api.model.entity.AExtensePO;
import com.lenovo.lmrp.api.model.entity.EntityMetadataPO;
import com.lenovo.lmrp.api.model.entity.FieldMetadataPO;
import com.lenovo.lmrp.base.LowerCaseMap;
import com.lenovo.lmrp.server.itsmgateway.repository.ARepositoryFactory;
import com.lenovo.sdc.framework.util.CommonUtil;
import com.lenovo.sdc.framework.util.exception.ErrorCode;
import com.lenovo.sdc.framework.util.exception.TraceableException;

/**
 * Convert ITSM entity to platform entitys base on entity metadata configuration
 * 
 * @author wujin
 * @version v1.0
 */
public class ItsmEntityTranslator {
    public static enum EntityName {
        TICKET, PROJECT, ENGINEER, PROJECTENGINEER, CLIENT, SP, OUTLET, ATTACHMENT;
    }
    
    private static final Logger logger = LoggerFactory.getLogger(ItsmEntityTranslator.class);

    private static Map<String, Map<String, Field>> poFieldMap = new ConcurrentHashMap<>();
    private static Map<EntityName, EntityMetadataPO> metadataMap = new ConcurrentHashMap<>();

    /**
     * load itsm entity metadata to convert raw data
     * 
     * @param itsmCode      registered ITSM identity
     * @param physicalName  ITSM entity name
     * @param rawData       ITSM entity raw data
     * @return
     */
    public <T> T translate(String itsmCode, EntityName physicalName, Map<String, String> rawData) {
        // get entity mapping definition
        EntityMetadataPO entityMetadata = getMetadataPO(itsmCode, physicalName);

        return translate(entityMetadata, rawData);
    }

    /**
     * convert row data data type base on metadata field mapping rule
     * because itsm required data different with ourself data type so we need to convert it before store in our db
     * 
     * @param itsmCode
     * @param physicalName
     * @param rawData
     * @return
     */
    public Map<String, Object> translateValue(String itsmCode, EntityName physicalName, Map<String, String> rawData) {
        // get entity mapping definition
        EntityMetadataPO entityMetadata = getMetadataPO(itsmCode, physicalName);

        Map<String, Object> result = new LowerCaseMap<>();
        rawData.forEach((key, value) -> result.put(key, value));

        for (FieldMetadataPO fm : entityMetadata.getFieldMetadataList()) {
            String value = rawData.get(fm.getPhysicalName());
            if (Objects.nonNull(value)) {
                try {
                    if (fm.getDataType() == MappedDT.INT.ordinal())
                        result.put(fm.getPhysicalName(), Integer.parseInt(value));
                    else if (fm.getDataType() == MappedDT.DATETIME.ordinal())
                        result.put(fm.getPhysicalName(), CommonUtil.getDateFormatter("yyyy-MM-dd HH:mm:ss")
                            .parse(value));
                    else if (fm.getDataType() == MappedDT.BOOLEAN.ordinal()) {
                        if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes")
                            || value.equalsIgnoreCase("y") || value.equals("1"))
                            result.put(fm.getPhysicalName(), true);
                        else
                            result.put(fm.getPhysicalName(), false);
                    }

                }
                catch (Exception exp) {
                    result.put(fm.getPhysicalName(), value);
                }
            }
        }

        return result;
    }

    private EntityMetadataPO getMetadataPO(String itsmCode, EntityName physicalName) {
        // get entity mapping definition
        EntityMetadataPO entityMetadata = metadataMap.get(physicalName);
        if (Objects.isNull(entityMetadata)) {
            synchronized (metadataMap) {
                entityMetadata = metadataMap.get(physicalName);
                if (Objects.isNull(entityMetadata)) {
                    ITicketSystemRepository repository = ARepositoryFactory.getFactory().getTicketSystemRepostory();
                    entityMetadata = repository.getEntityMetadata(itsmCode, physicalName);
                    metadataMap.put(physicalName, entityMetadata);
                }
            }
        }
        
        return entityMetadata;
    }

    /**
     * convert ITSM raw data to platform entity according to configured entityMetadata
     * 
     * @param entityMetadata    entity convert rule
     * @param rawData           ITSM entity raw data
     * @return
     */
    public <T> T translate(EntityMetadataPO entityMetadata, Map<String, String> rawData) {
        try {
            // get target entity field list
            Class<?> clz = Class.forName("com.lenovo.lmrp.api.model.entity." + entityMetadata.getMappedName());
            Map<String, Field> fieldMap = getPOFields(clz);

            //loop field metadata list
            Object po = clz.newInstance();
            for (FieldMetadataPO fm : entityMetadata.getFieldMetadataList()) {

                // find target field that defined in mappedname of field metadata
                Field field = fieldMap.get(fm.getMappedName());

                String value = rawData.get(fm.getPhysicalName());
                if (Objects.nonNull(value))
                    value = value.trim();

                if (logger.isDebugEnabled())
                    logger.debug("{}={}", fm.getPhysicalName(), value);

                if (fm.getMappedName().contains(".")) {
                    // mappedname contain '.' means that is PO field, we should instance that po then set its field
                    String[] clzfd = fm.getMappedName().split("[\\.]");
                    Field subpo = fieldMap.get(clzfd[0]);
                    Map<String, Field> subPoFields = getPOFields(subpo.getType());

                    // create po instance
                    subpo.setAccessible(true);
                    if (subpo.get(po) == null) {
                        subpo.set(po, subpo.getType().newInstance());
                    }

                    // set po field value
                    Field subfd = subPoFields.get(clzfd[1]);
                    setFieldValue(subpo.get(po), fm, subfd, value);
                }
                else if (Objects.isNull(field) || fm.isExtense()) {
                    // not found mapped field or map to extense field, value put to others field
                    Field others = fieldMap.get("others");
                    if (Objects.isNull(others))
                        continue;

                    // instante others field object that should be sub-class of AExtensePO
                    others.setAccessible(true);
                    if (others.get(po) == null) {
                        others.set(po, others.getType().newInstance());
                    }

                    // put value to AExtensePO
                    AExtensePO extense = (AExtensePO) others.get(po);
                    appendExtenseProperty(extense, fm, value);
                }
                else {
                    setFieldValue(po, fm, field, value);
                }
            }

            return (T) po;
        }
        catch (Exception exp) {
            throw new TraceableException(ErrorCode.COMM_PARSE_FAILED, exp);
        }
    }

    /**
     * use reflect to obtain PO field list and cached to improve performance
     * 
     * @param clz
     * @return
     * @throws Exception
     */
    private final Map<String, Field> getPOFields(Class<?> clz) throws Exception {
        Map<String, Field> fieldMap = poFieldMap.get(clz.getName());
        if (Objects.isNull(fieldMap)) {
            fieldMap = new HashMap<>();
            Field[] fields = clz.getDeclaredFields();
            for (Field field : fields) {
                fieldMap.put(field.getName().toLowerCase(), field);
            }
            Map<String, Field> old = poFieldMap.put(clz.getName(), fieldMap);
            if (Objects.nonNull(old))
                old.clear();
        }

        return fieldMap;
    }

    /**
     * put raw data to exense field
     * 
     * @param extense
     * @param fm
     * @param value
     * @throws Exception
     */
    private final void appendExtenseProperty(AExtensePO extense, FieldMetadataPO fm, String value) throws Exception {
        if (fm.getDataType() == MappedDT.STRING.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), null);
            else
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), value);
        }
        else if (fm.getDataType() == MappedDT.BOOLEAN.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), false);
            else {
                if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("y")
                    || value.equals("1"))
                    extense.getAttributeBean().addAttribute(fm.getPhysicalName(), true);
                else
                    extense.getAttributeBean().addAttribute(fm.getPhysicalName(), false);
            }
        }
        else if (fm.getDataType() == MappedDT.INT.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), 0);
            else
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), Integer.parseInt(value));
        }
        else if (fm.getDataType() == MappedDT.DOUBLE.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), 0.0);
            else
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), Double.parseDouble(value));
        }
        else if (fm.getDataType() == MappedDT.DATETIME.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(), null);
            else
                extense.getAttributeBean().addAttribute(fm.getPhysicalName(),
                    CommonUtil.getDateFormatter("yyyy-MM-dd HH:mm:ss").parse(value));
        }
        else
            extense.getAttributeBean().addAttribute(fm.getPhysicalName(), value);
    }

    /**
     * convert raw data to PO field value
     * 
     * @param po
     * @param fm
     * @param field
     * @param value
     * @throws Exception
     */
    private final void setFieldValue(Object po, FieldMetadataPO fm, Field field, String value) throws Exception {
        field.setAccessible(true);
        if (fm.getDataType() == MappedDT.STRING.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                field.set(po, null);
            else
                field.set(po, value);
        }
        if (fm.getDataType() == MappedDT.BOOLEAN.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                field.set(po, false);
            else {
                if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("y")
                    || value.equals("1"))
                    field.set(po, true);
                else
                    field.set(po, false);
            }
        }
        else if (fm.getDataType() == MappedDT.INT.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                if (field.getType().isEnum())
                    setEnumValue(field, po, "0");
                else
                    field.set(po, Integer.parseInt(value));
            else {
                if (field.getType().isEnum())
                    setEnumValue(field, po, value);
                else
                    field.set(po, Integer.parseInt(value));
            }
        }
        else if (fm.getDataType() == MappedDT.DOUBLE.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                field.set(po, 0.0);
            else
                field.set(po, Double.parseDouble(value));
        }
        else if (fm.getDataType() == MappedDT.DATETIME.ordinal()) {
            if (Objects.isNull(value) || value.equals("null"))
                field.set(po, null);
            else
                field.set(po, CommonUtil.getDateFormatter("yyyy-MM-dd HH:mm:ss").parse(value));
        }
    }
    
    private void setEnumValue(Field field, Object po, String value) throws Exception {
        int code = Integer.parseInt(value);
        Object[] es = field.getType().getEnumConstants();
        for (Object e : es) {
            Object ev = e.getClass().getMethod("getCode", null).invoke(e, null);
            if (ev.equals(code)) {
                field.set(po, e);
                return;
            }
        }
    }
}
